package org.timeticker;

import org.apache.log4j.Logger;
import org.timeticker.impl.ActivityListImpl;
import org.timeticker.DayRange;
import org.timeticker.impl.ProjectListImpl;
import org.timeticker.impl.WorkTypeListImpl;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Time Ticker core
 *
 * @author Ivan Latysh
 * @since 11-Jul-2007 : 9:47:37 PM
 * @version 0.1
 */
public class Timekeeper {
  // Error prefix TTIC00009

  /** Our instance instance */
  private static Timekeeper ourInstance = new Timekeeper();

  /** Work type list key */
  public static final String WORK_TYPE_LIST = WorkTypeList.class.getName();
  /** ProjectDialog list key */
  public static final String PROJECT_LIST = ProjectList.class.getName();

  /**
   * Return Instance
   *
   * @return instance
   */
  public static Timekeeper getInstance() {
    return ourInstance;
  }

  /** Private constructor */
  private Timekeeper() { }

  /** Component logger */
  protected Logger logger = Logger.getLogger(this.getClass().getName());
  /** Activity listeners */
  protected final List<ActivityListener> activityListeners = new ArrayList<ActivityListener>();
  /** Timekeeper listeners */
  protected final List<TimekeeperActivityListListener> timekeeperActivityListListeners = new ArrayList<TimekeeperActivityListListener>();
  /** Activity list */
  protected ActivityList activities;
  /** Object lists map */
  protected final Map<String, ObjectList> objectLists = new HashMap<String, ObjectList>();
  /** Activity key date format */
  public static final String activityKeyDateFormat = "yyyyMMdd";    
  /** Activity key pattern */
  public static final String activityKeyPattern = "\\p{Digit}{8}";

  /**
   * Return object list with given key
   *
   * @param key list key
   * @return object list
   * @see #WORK_TYPE_LIST
   */
  public ObjectList getObjectList(String key) {
    return objectLists.get(key);
  }

  /**
   * Add object list with given key. If list with a key already exists it will be replaced.
   *
   * @param key list key
   * @param list object list
   */
  public void putObjectList(String key, ObjectList list) {
    objectLists.put(key, list);
  }

  /**
   * Lifecycle: Initialize TimeTicker.
   *
   * @see #destroy()
   */
  public void initialize() {
    Persistence pers = PersistenceManager.getInstance();
    // load today activities
    if (null==activities) {
      Day today = new Day();
      loadActivityList(new DayRange(today, today));
    }

    // load WorkTypeList
    WorkTypeList wtList = null;
    try {
      if (pers.isObjectListExists(WORK_TYPE_LIST)) {
        wtList = (WorkTypeList) pers.restoreObjectList(WORK_TYPE_LIST);
      }
    } catch (Exception e) {
      logger.error("[TTIC00003] Unable to load work types.", e);
    }
    if (null==wtList) wtList = new WorkTypeListImpl();
    putObjectList(WORK_TYPE_LIST, wtList);

    // load ProjectList
    ProjectList projects = null;
    try {
      if (pers.isObjectListExists(PROJECT_LIST)) {
        projects = (ProjectList) pers.restoreObjectList(PROJECT_LIST);
      }
    } catch (Exception e) {
      logger.error("[TTIC00006] Unable to load projects.", e);
    }
    if (null==projects) projects= new ProjectListImpl();
    putObjectList(PROJECT_LIST, projects);

    // logs
    logger.info(this.getClass().getName()+" initialized.");
  }

  /**
   * Lifecycle: Destroy TimeTicker.
   *
   * @see #initialize()
   */
  public void destroy() {
    Persistence pers = PersistenceManager.getInstance();
    // save activities
    try {
      saveActivities(activities);
    } catch (Exception e) {
      logger.error("[TTIC00005] Unable to save activity list.", e);
    }
    // save work types
    try {
      pers.persistObjectList(WORK_TYPE_LIST, getObjectList(WORK_TYPE_LIST));
    } catch (Exception e) {
      logger.error("[TTIC00004] Unable to persist work types.", e);
    }
    // save projects
    try {
      pers.persistObjectList(PROJECT_LIST, getObjectList(PROJECT_LIST));
    } catch (Exception e) {
      logger.error("[TTIC00007] Unable to persist projects.", e);
    }
    // logs
    logger.info(this.getClass().getName()+" destroyed.");
  }

  /**
   * Return known worked days
   *
   * @return date list
   */
  public long[] getKnownWorkedDays() {
    Persistence pers = PersistenceManager.getInstance();
    SimpleDateFormat activityKeyFormat = new SimpleDateFormat(activityKeyDateFormat);
    List<String> keyList = pers.getKeyList(activityKeyPattern);
    long[] keys = new long[keyList.size()];
    int i=0;
    for (String s: keyList) {
      try {
        keys[i]=activityKeyFormat.parse(s).getTime();
      } catch (ParseException e) {
        logger.error("[TTIC00008] Unable to parse date {"+s+"}", e);
      }
      i++;
    }    
    return keys;
  }

  /**
   * Return current activity list
   *
   * @return activity list
   */
  public final ActivityList getActivityList() {
    return activities;
  }

  /**
   * Load activities for given date range
   *
   * @param range date range
   */
  public void loadActivityList(DayRange range) {
    ActivityList _activities = loadActivities(range);
    if (null==_activities) _activities = newActivityList(range);
    // save current activities if needed
    if (null!=activities) {
      if (activities.isModified()) saveActivities(activities);
      // remove listeners
      for (ActivityListener l : activityListeners) {
        activities.removeActivityListener(l);
      }
    }
    // update activity list
    activities = _activities;
    // set listeners
    for (ActivityListener l : activityListeners) {
      activities.addActivityListener(l);
    }
    // fire activities loaded event
    fireActivitiesLoaded(activities);
  }

  /**
   * Add activity listener
   *
   * @param l listener to add
   */
  public void addActivityListener(ActivityListener l) {
    // add listener to our list
    synchronized(activityListeners) {
      activityListeners.add(l);
    }
    // register listener in current activity list
    activities.addActivityListener(l);
  }

  /**
   * Remove activity listener
   *
   * @param l listener to remove
   */
  public void removeActivityListener(ActivityListener l) {
    // remove listener from current activity list
    activities.removeActivityListener(l);
    // remove listener from our list
    synchronized(activityListeners) {
      activityListeners.remove(l);
    }
  }

  /**
   * Add timekeeper listener
   *
   * @param l listener to add
   */
  public void addListener(TimekeeperActivityListListener l) {
    timekeeperActivityListListeners.add(l);
  }

  /**
   * Remove timekeeper listener
   *
   * @param l listener to remove
   */
  public void removeListener(TimekeeperActivityListListener l) {
    timekeeperActivityListListeners.remove(l);
  }

  /**
   * Fire activities loaded event
   *
   * @param activities loaded activities
   */
  protected void fireActivitiesLoaded(final ActivityList activities) {
    for (TimekeeperActivityListListener l : timekeeperActivityListListeners) {
      l.loadedActivityList(activities);
    }
  }

  /**
   * Fire activities saved event
   *
   * @param activities saved activities
   */
  protected void fireActivitiesSaved(final ActivityList activities) {
    for (TimekeeperActivityListListener l : timekeeperActivityListListeners) {
      l.savedActivityList(activities);
    }
  }

  /**
   * Load and return activity for given date
   *
   * @param range date
   * @return activity list
   */
  private ActivityList loadActivities(DayRange range) {
    // get persistence
    Persistence pers = PersistenceManager.getInstance();
    SimpleDateFormat activityKeyFormat = new SimpleDateFormat(activityKeyDateFormat);
    // new ActivityList
    ActivityList activities = newActivityList(range);
    // create date from calendar that will be rolling
    Calendar calendar = Calendar.getInstance(Locale.getDefault());
    calendar.setTime(activities.getDateRange().getFrom());
    // create date to calendar
    Calendar dateTo = Calendar.getInstance(Locale.getDefault());
    dateTo.setTime(activities.getDateRange().getTo());
    // load activities for given range
    while (calendar.before(dateTo) || calendar.equals(dateTo)) {
      String key = activityKeyFormat.format(calendar.getTime());
      if (pers.isObjectListExists(key)) {
        try {          
          activities.addDailyActivity((DailyActivityList) pers.restoreObjectList(key));
        } catch (Exception e) {
          logger.error("[TTIC00001] Unable to load activity list for {"+ key +"}", e);
        }
      } else {
        logger.debug("Activity List does not exist for {"+ key +"}");
      }
      calendar.add(Calendar.DAY_OF_MONTH, 1);
    }
    // reset modified flag
    activities.setModified(false);
    // return activities
    return activities;
  }

  /**
   * Save activity list
   *
   * @param activities activity list to save
   */
  private void saveActivities(ActivityList activities) {
    // get persistence manager
    Persistence pers = PersistenceManager.getInstance();
    // get date formatter
    SimpleDateFormat activityKeyFormat = new SimpleDateFormat(activityKeyDateFormat);
    // create date from calendar that will be rolling
    Calendar calendar = Calendar.getInstance(Locale.getDefault());
    calendar.setTime(activities.getDateRange().getFrom());
    // create date to calendar
    Calendar dateTo = Calendar.getInstance(Locale.getDefault());
    dateTo.setTime(activities.getDateRange().getTo());
    // save all daily activities 
    while (calendar.before(dateTo) || calendar.equals(dateTo)) {
      DailyActivityList da = activities.getDailyActivity(new Day(calendar.getTime().getTime()));
      if (null!=da) {
        String key = activityKeyFormat.format(da.getDay());
        try {
          pers.persistObjectList(key, da);
        } catch (Exception e) {
          logger.error("[TTIC00002] Unable to save activity list {"+da+"}", e);
        }
      }
      calendar.add(Calendar.DAY_OF_MONTH, 1);
    }
    // update modified flag
    activities.setModified(false);
    // fire activities saved event
    fireActivitiesSaved(activities);    
  }

  /**
   * Construct a new activitylist
   *
   * @param range activity date or null
   * @return new activity list
   */
  private ActivityList newActivityList(DayRange range) {
    Day now = new Day();
    if (null==range) range = new DayRange(now, now);
    ActivityList _a = new ActivityListImpl();
    _a.setDateRange(range);
    return _a;
  }

}
